﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration;
using System.Configuration.Provider;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Web.Configuration;
using System.Web.Security;

using OpenFaq.Core;
using OpenFaq.Core.UserModule;
using OpenFaq.Infrastructure;

namespace OpenFaq.Application
{
	public class CustomMembershipProvider : MembershipProvider
	{
		private string _applicationName;
		private bool _enablePasswordReset;
		private bool _enablePasswordRetrieval;
		private bool _requiresQuestionAndAnswer;
		private int _maxInvalidPasswordAttempts;
		private int _passwordAttemptWindow;
		private bool _requiresUniqueEmail;

		private MembershipPasswordFormat _passwordFormat;
		private int _minRequiredPasswordLength;
		private int _minRequiredNonAlphanumericCharacters;

		private string _passwordStrengthRegularExpression;

		private MachineKeySection _machineKey;


		#region Propertires
		public override string ApplicationName
		{
			get { return _applicationName; }
			set { _applicationName = value; }
		}

		public override bool EnablePasswordRetrieval
		{
			get { return _enablePasswordReset; }
		}

		public override bool EnablePasswordReset
		{
			get { return _enablePasswordRetrieval; }
		}

		public override bool RequiresQuestionAndAnswer
		{
			get { return _requiresQuestionAndAnswer; }
		}


		public override int MaxInvalidPasswordAttempts
		{
			get { return _maxInvalidPasswordAttempts; }
		}

		public override int PasswordAttemptWindow
		{
			get { return _passwordAttemptWindow; }
		}

		public override bool RequiresUniqueEmail
		{
			get { return _requiresUniqueEmail; }
		}

		public override MembershipPasswordFormat PasswordFormat
		{
			get { return _passwordFormat; }
		}

		public override int MinRequiredPasswordLength
		{
			get { return _minRequiredPasswordLength; }
		}

		public override int MinRequiredNonAlphanumericCharacters
		{
			get { return _minRequiredNonAlphanumericCharacters; }
		}

		public override string PasswordStrengthRegularExpression
		{
			get { return _passwordStrengthRegularExpression; }
		}

		#endregion 

		public override void Initialize(string name, NameValueCollection config)
		{
			//
			// Initialize values from web.config.
			//
			Verify.Argument.IsNotNull(config, "config");

			if (config == null)
				throw new ArgumentNullException("config");

			if (String.IsNullOrEmpty(name))
				name = "CustomMembershipProvider";

			if (String.IsNullOrEmpty(config["description"]))
			{
				config.Remove("description");
				config.Add("description", "Custom Membership provider");
			}

			// Initialize the abstract base class.
			base.Initialize(name, config);

			_applicationName = GetConfigValue(config["applicationName"],
											System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath);
			_enablePasswordReset = Convert.ToBoolean(GetConfigValue(config["enablePasswordReset"], "true"));

			_enablePasswordRetrieval = Convert.ToBoolean(GetConfigValue(config["enablePasswordRetrieval"], "true"));
			_requiresQuestionAndAnswer = Convert.ToBoolean(GetConfigValue(config["requiresQuestionAndAnswer"], "false"));
			
			_maxInvalidPasswordAttempts = Convert.ToInt32(GetConfigValue(config["maxInvalidPasswordAttempts"], "5"));
			_passwordAttemptWindow = Convert.ToInt32(GetConfigValue(config["passwordAttemptWindow"], "10"));
			_requiresUniqueEmail = Convert.ToBoolean(GetConfigValue(config["requiresUniqueEmail"], "true"));
			
			_minRequiredNonAlphanumericCharacters = Convert.ToInt32(GetConfigValue(config["minRequiredNonAlphanumericCharacters"], "1"));
			_minRequiredPasswordLength = Convert.ToInt32(GetConfigValue(config["minRequiredPasswordLength"], "6"));
			_passwordStrengthRegularExpression = Convert.ToString(GetConfigValue(config["passwordStrengthRegularExpression"], ""));
		
			string tempFormat = config["passwordFormat"];
			if (tempFormat == null)
				tempFormat = "Hashed";

			switch (tempFormat)
			{
				case "Hashed":
					_passwordFormat = MembershipPasswordFormat.Hashed;
					break;
				case "Encrypted":
					_passwordFormat = MembershipPasswordFormat.Encrypted;
					break;
				case "Clear":
					_passwordFormat = MembershipPasswordFormat.Clear;
					break;
				default:
					throw new ProviderException("Password format not supported.");
			}


			//Get encryption and decryption key information from the configuration.
			//Configuration cfg =
			//WebConfigurationManager.OpenWebConfiguration(System.Web.Hosting.HostingEnvironment.ApplicationVirtualPath);
			//_machineKey = (MachineKeySection)cfg.GetSection("system.web/machineKey");

			_machineKey = (MachineKeySection)ConfigurationManager.GetSection("system.web/machineKey");

			if (_machineKey.ValidationKey.Contains("AutoGenerate"))
				if (PasswordFormat != MembershipPasswordFormat.Clear)
					throw new ProviderException("Hashed or Encrypted passwords " +
												"are not supported with auto-generated keys.");

			
		}

		/// <summary>
		/// A helper function to retrieve config values from the configuration file.
		/// </summary>
		/// <param name="configValue"></param>
		/// <param name="defaultValue"></param>
		/// <returns></returns>
		private string GetConfigValue(string configValue, string defaultValue)
		{
			if (String.IsNullOrEmpty(configValue))
				return defaultValue;

			return configValue;
		}


		public override MembershipUser CreateUser(string username,
				 string password,
				 string email,
				 string passwordQuestion,
				 string passwordAnswer,
				 bool isApproved,
				 object providerUserKey,
				 out MembershipCreateStatus status)
		{

			Verify.Argument.IsNotEmpty(username, "username");
			Verify.Argument.IsNotEmpty(password, "password");
			Verify.Argument.IsNotEmpty(email, "email");

			if (this.RequiresQuestionAndAnswer)
			{
				Verify.Argument.IsNotEmpty(passwordQuestion, "passwordQuestion");
				Verify.Argument.IsNotEmpty(passwordAnswer, "passwordAnswer");
			}

			if (!IsValidPassword(password))
			{
				status = MembershipCreateStatus.InvalidPassword;
				return null;
			}

			ValidatePasswordEventArgs args = new ValidatePasswordEventArgs(username, password, true);
			OnValidatingPassword(args);

			if (args.Cancel)
			{
				status = MembershipCreateStatus.InvalidPassword;
				return null;
			}


			if (RequiresUniqueEmail)
			{
				using (var context = new OpenFaqContext())
				{
					var emailUser = context.Users.FirstOrDefault(usr => usr.Email == email);
					if (emailUser != null)
					{
						status = MembershipCreateStatus.DuplicateEmail;
						return null;
					}
				}
				
			}

			MembershipUser user = GetUser(username, false);
			if (user != null)
			{
				status = MembershipCreateStatus.DuplicateUserName;
				return null;
			}

			
			DateTime createDate = DateTime.Now;
			User newUser = null;
			using (var context = new OpenFaqContext())
			{
				newUser = new User
				{
					Username = username,
					Password = EncodePassword(password),
					Email = email,
					PasswordQuestion = passwordQuestion,
					PasswordAnswer = passwordAnswer,
					IsApproved = isApproved,
					DateCreated = createDate,
					LastPasswordChangedDate = createDate,
					LastActivityDate = createDate,
					IsLockedOut = false,
					LastLockedOutDate = createDate,
					FailedPasswordAttemptCount =0,
					FailedPasswordAttemptWindowStart = createDate,
					FailedPasswordAnswerAttemptCount = 0,
					FailedPasswordAnswerAttemptWindowStart = createDate,
				};

				context.Users.Add(newUser);
				context.SaveChanges();
			}

			status = MembershipCreateStatus.Success;
			return GetUser(username, false);
		}

		private bool IsValidPassword(string password)
		{
			//Any logic to process password validation conditions
			//e.g:
			if (password.Length < MinRequiredPasswordLength)
				return false;

			if (MinRequiredNonAlphanumericCharacters > 0)
			{
				int nonAlpahNumericCharsCount = Regex.Matches(password, "[^a-zA-Z0-9]").Count;
				if (nonAlpahNumericCharsCount < MinRequiredNonAlphanumericCharacters)
					return false;
			}

			if (!String.IsNullOrEmpty(PasswordStrengthRegularExpression))
			{
				if (!Regex.IsMatch(password, this.PasswordStrengthRegularExpression))
					return false;
			}

	
			return true;
		}


		public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer)
		{
			throw new NotImplementedException();
		}

		public override string GetPassword(string username, string answer)
		{
			throw new NotImplementedException();
		}

		public override bool ChangePassword(string username, string oldPassword, string newPassword)
		{
			Verify.Argument.IsNotEmpty(username, "username");
			Verify.Argument.IsNotEmpty(oldPassword, "oldPassword");
			Verify.Argument.IsNotEmpty(newPassword, "newPassword");

			if (!ValidateUser(username, oldPassword))
				return false;

			if (!IsValidPassword(newPassword))
				return false;

			using (var db = new OpenFaqContext())
			{
				User user = db.Users.Single(usr => usr.Username == username);
				user.Password = EncodePassword(newPassword);
				user.LastPasswordChangedDate = DateTime.UtcNow;

				db.SaveChanges();
			}

			return true;
		}

		public override string ResetPassword(string username, string answer)
		{
			throw new NotImplementedException();
		}

		public override void UpdateUser(MembershipUser user)
		{
			throw new NotImplementedException();
		}

		public override bool ValidateUser(string username, string password)
		{
			Verify.Argument.IsNotEmpty(username, "username");
			Verify.Argument.IsNotEmpty(password, "password");

			using (var db = new OpenFaqContext())
			{
				string encodedPassword = EncodePassword(password);

				User user = db.Users.SingleOrDefault(u => u.Username == username && u.Password == encodedPassword);

				if (user != null)
					return true;

				return false;
			}
		}


		public override bool UnlockUser(string userName)
		{
			throw new NotImplementedException();
		}

		public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
		{
			throw new NotImplementedException();
		}

		public override MembershipUser GetUser(string username, bool userIsOnline)
		{
			Verify.Argument.IsNotEmpty(username, "userName");

			using (var context = new OpenFaqContext())
			{
				User user = context.Users.FirstOrDefault(usr => usr.Username == username);
				if (user == null)
					return null;

				return new MembershipUser
							(this.Name, username, user.Id, user.Email, null, null, true, false,
							user.DateCreated, DateTime.MinValue, DateTime.MinValue, DateTime.MinValue, DateTime.MinValue);
			}
		}

		public override string GetUserNameByEmail(string email)
		{
			throw new NotImplementedException();
		}

		public override bool DeleteUser(string username, bool deleteAllRelatedData)
		{
			throw new NotImplementedException();
		}

		public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
		{
			throw new NotImplementedException();
		}

		public override int GetNumberOfUsersOnline()
		{
			throw new NotImplementedException();
		}

		public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
		{
			throw new NotImplementedException();
		}

		public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
		{
			throw new NotImplementedException();
		}


		//Password encoding
		private string EncodePassword(string password)
		{
			string encodedPassword = password;

			switch (PasswordFormat)
			{
				case MembershipPasswordFormat.Clear:
					break;
				case MembershipPasswordFormat.Encrypted:
					encodedPassword =
					  Convert.ToBase64String(EncryptPassword(Encoding.Unicode.GetBytes(password)));
					break;
				case MembershipPasswordFormat.Hashed:
					HMACSHA1 hash = new HMACSHA1();
					hash.Key = HexToByte(_machineKey.ValidationKey);
					encodedPassword =
					  Convert.ToBase64String(hash.ComputeHash(Encoding.Unicode.GetBytes(password)));
					break;
				default:
					throw new ProviderException("Unsupported password format.");
			}

			return encodedPassword;
		}

		/// <summary>
		///  Decrypts or leaves the password clear based on the PasswordFormat.
		/// </summary>
		/// <param name="encodedPassword"></param>
		/// <returns></returns>
		private string UnEncodePassword(string encodedPassword)
		{
			string password = encodedPassword;

			switch (PasswordFormat)
			{
				case MembershipPasswordFormat.Clear:
					break;
				case MembershipPasswordFormat.Encrypted:
					password =
					  Encoding.Unicode.GetString(DecryptPassword(Convert.FromBase64String(password)));
					break;
				case MembershipPasswordFormat.Hashed:
					throw new ProviderException("Cannot unencode a hashed password.");
				default:
					throw new ProviderException("Unsupported password format.");
			}

			return password;
		}

		//
		// HexToByte
		//   Converts a hexadecimal string to a byte array. Used to convert encryption
		// key values from the configuration.
		//

		private byte[] HexToByte(string hexString)
		{
			byte[] returnBytes = new byte[hexString.Length / 2];
			for (int i = 0; i < returnBytes.Length; i++)
				returnBytes[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);
			return returnBytes;
		}

	}
}
